ECOM: plugins for Symbian OSECOM: plugins for Symbian OS
Rafał Kocisz
My aim in writing this article was to provide a clear and
simple yet detailed introduction to ECOM, and to this end I\'ve put
together a simple (though non-trivial) application demonstrating the
mechanisms discussed. The sample program provides the basis for a
step-by-step tutorial on creating an ECOM plugin. The source code and
architecture of the application are carefully designed and thoroughly
tested, so they can be used to build both training programs and
production-quality applications.
Before we begin...
Developing software for the Symbian OS is a fairly
complex and challenging task, especially for beginners. There are
several reasons for this, starting with the necessity of working in an
emulated development environment. An additional problem is the relative
scarcity of good literature on Symbian development. Not that I\'m trying
to discourage you. Far from it - in writing this article, I wanted to
show that even such complex issues as plugin development can be
mastered quite easily, of course provided you have the necessary
background knowledge. The article is not intended as a starting point
for Symbian programming, so if you\'re completely unfamiliar with the
platform I suggest you catch up on the basics first. A good place to
start would be the Symbian workshop in the May 2005 issue of SDJ, as it
provides a friendly and concise introduction to the essentials of
Symbian development.
The approach I\'ve adopted for this article is that
information not directly related to ECOM, but useful for understanding
the big picture, can be found in Frames throughout the article.
Hopefully this will make the key concepts easier to understand for
everyone less familiar with Symbian while keeping the workshop
focussed. It\'s also worth having a look at the web resources listed
towards the end of the article, and anyone intent on seriously getting
into Symbian development would do well to get their hands on the books
listed in the Frame Further reading. If you have any questions
concerning the article, feel free to contact me.
Plugin framework
Most software users are intuitively familiar with the
concept of plugins as dynamically added extensions to core application
functionality. Many popular programs - most notably media players -
support plugins that extend their basic capabilities (for example
adding support for new file formats without reinstalling the whole
application) or modify the application on the fly without interfering
with the source code (for example changing the user interface using
skins). More advanced plugins can be found in modern web browsers, such
as Firefox or Opera.
More formally, a plugin could be defined as a
self-contained piece of executable code that can be dynamically linked
into an existing application to modify or extend its functionality. Of
course, a plugin is not much use without application code to support
it. From a programmer\'s point of view, the key concept here is that of
a plugin framework to provide this functionality. The framework should:
-
define an interface for plugins;
-
support one or more implementations of the plugin interface (i.e. actual plugins) that will only be loaded at runtime;
-
provide the client with a plugin management capability, covering such actions as loading, destroying and distinguishing plugins.
Once such a framework is ready, adding and supporting
plugins is easy. Now for the good news: in Symbian OS, a plugin
framework is integrated into the system, just waiting to be used. Let\'s
see how it\'s done.
Creating the IFTool application
To make this ECOM tutorial easier and more engaging, I\'ve
decided to prepare a relatively simple yet non-trivial and genuinely
useful project to illustrate the stages involved in working with
Symbian plugins. The source code is freely available and you are
encouraged to experiment with the application and extend its basic
functionality.
The program is called IFTool, which is short for Image
Filtering Tool. IFTool is a simple graphics manipulation utility that
can load an image file (typically from permanent storage), perform
basic transformations on the image (i.e. filter it) and write the
modified file back to storage. What for? - I hear you ask. Well, if
you\'ve ever taken photos using a mobile phone camera you will know they
are usually of rather poor quality, especially in low light conditions.
However, running such a photo through some elementary filters (even
just a basic averaging filter) can significantly improve the quality,
so having a simple graphics utility on your phone makes it possible to
quickly send the improved photo to your friends by MMS or upload it to
your blog.
For the purpose of this article, the most important
feature of IFTool is that each filter is implemented as a plugin, so
both the author and potential users of the program have the option of
easily adding more filters in the future. The basic version presented
in the article will offer two ECOM-based filters. Adding other filters
is left as an exercise for the reader, as suggested later in the
article.
The sample application was developed for the Series 60
platform with Symbian 7.0 or later, but as ECOM is an integral part of
the core Symbian system, the same techniques can be used when
developing software for other Symbian-based platforms, such as UIQ.
Figure 1 presents sample screenshots of the finished
application, while the Frame Quickstart presents the steps necessary to
build and run the program.
Figure 1. Screenshots of the IFTool program
Before we get to the actual workshop part, I\'d like to
make a quick disclaimer: I am not an expert in image processing, so I
realise that my graphics filter implementations are far from perfect.
However, I can assure you that the IFTool application itself
(especially code dealing with plugins) was designed and developed with
utmost care and attention to quality.
Introduction to ECOM
As already mentioned, a plugin support framework consists
of three parts: a plugin interface, any number of plugins implementing
this interface and a plugin management subsystem. While the first two
can relatively easily be implemented in Symbian using a dynamic link
library (DLL) with a polymorphic interface (see Frame DLLs in Symbian
OS), the third part is much more tricky - implementing a plugin
management solution that would be generic, secure and portable across
the numerous Symbian-based platforms is very difficult.
DLLs in Symbian OS
A dynamic link library (DLL) is a piece of code that is
linked into an application at runtime, unlike static libraries, which
are linked at compile time. Symbian OS supports two kinds of DLLs:
A DLL with a static interface is automatically loaded
into device memory when required (for example when a program that uses
it is loaded) and is automatically unloaded when no longer needed.
Static interface libraries are typically used to separate application
logic from the user interface when implementing application engines.
A DLL with a polymorphic interface has to be explicitly loaded from client code by calling the RLibrary::Load() method and explicitly released using the RLibrary::Close() method.
Multiple libraries of this type can implement the same interface, so
they are commonly used when working with plugin frameworks.
Client-server architecture in Symbian OS
The client-server architecture integrated into Symbian
OS is one of the fundamental design idioms for the platform. The idea
itself is nothing new - we have a server providing some services and a
client making use of them. However, while the architecture is usually
associated with networking, in Symbian OS it provides the basis for
process interaction within the system, only the clients and servers are
system processes rather than network hosts.
The client-server architecture is one of the factors
contributing to the robustness of Symbian OS. All resource access is
implemented in this way, meaning that all clients are confined to using
specific APIs within their respective processes and therefore cannot
(accidentally or otherwise) gain arbitrary access to the system kernel.
Limited versions of this approach are also used in other
operating systems. In Unix-based systems, for example, the multilayered
structure means that the system kernel in only accessible through the
strict API of system calls. However, the designers of Symbian OS went a
step further, allowing developers to leverage the system\'s
client-server architecture for their own purposes and thus opening up
some very interesting possibilities for Symbian application
development. You can find more information in the books listed under
Further reading.
The creators of Symbian were no doubt aware of this issue
and addressed it by developing ECOM - a generic, extensible plugin
framework that makes it possible to:
-
identify specific plugin implementations for specific interfaces,
-
select an implementation to use,
-
create an object for the selected implementation and pass it to the plugin client,
-
dispose of the plugin object when it is no longer needed.
Figure 2 presents the architecture and general workings
of the ECOM framework, detailing the particular elements of the plugin
subsystem and the relations between them. ECOM transparently locates
and instantiates the relevant plugin implementation and provides the
client with the resulting object, accessible via operations defined for
the plugin interface. The framework uses a client-server architecture
(see Frame Client-server architecture in Symbian OS), so ECOM-managed
plugins can be loaded in parallel by independent client processes. ECOM
can also automatically monitor the file system for the appearance of
new plugins and features an integrated reference-counting mechanism,
allowing unused resources to be automatically deallocated. All this
functionality is completely hidden from the programmer. The remainder
of the article presents the implementation of ECOM-compliant plugins
for the IFTool utility.
Figure 2. ECOM architecture
The ECOM interface
Let\'s start by looking at the process of implementing the ECOM plugin interface.
Listing 1. ECOM plugin interface for the CIFToolPlugIn class
#ifndef __IFTOOL_PLUG_IN_H__
#define __IFTOOL_PLUG_IN_H__
#include <E32Base.h>
#include <ECom.h>
#include <Gdi.h>
// UID for the ECOM interface
const TUid KCIFToolPlugIn_InterfaceUid = { 0x20000AC8 };
class CFbsBitmap;
class CIFToolPlugIn : public CBase
{
public:
// Create default plugin
IMPORT_C static CIFToolPlugIn* NewL();
// Create plugin indicated by the argument
IMPORT_C static CIFToolPlugIn* NewL(
const TDesC8& aCue );
// Destructor
IMPORT_C virtual ~CIFToolPlugIn();
// Build a list of all available implementations
// of this ECOM interface
IMPORT_C static void ListImplementationsL(
RImplInfoPtrArray& aImplInfoArray );
// Filter the source bitmap and draw the result
// to the context in the second argument
IMPORT_C void DoFilterL(
const CFbsBitmap& aSourceBitmap,
CGraphicsContext& aOutputGc );
protected:
IMPORT_C CIFToolPlugIn();
private:
// Filter a single pixel and return its colour
IMPORT_C virtual TRgb FilterPixelL(
const CFbsBitmap& aSourceBitmap,
TPoint aPixelPoint ) = 0;
private:
// Required by ECOM
TUid iDtor_ID_Key;
};
#endif // __IFTOOL_PLUG_IN_H__
The code in Listing 1 defines the abstract class
corresponding the plugin interface for the IFTool utility. Before we
get into working with ECOM itself, a word needs to be said on how
plugins are supported within the sample application. For the graphics
filter plugin client (i.e. IFTool), the most important method is
DoFilterL():
IMPORT_C void DoFilterL(
const CFbsBitmap& aSourceBitmap,
CGraphicsContext& aOutputGc );
The method takes two arguments: the source bitmap
corresponding to the image to be filtered and a graphics context
instance that the filtered image will be written to. My choice of
second argument might at first seem arguable, as specifying a target
bitmap instead would appear to be a simpler solution. However, using a
graphics context provides more flexibility - CGraphicsContext is an
abstract class, so the DoFilterL() method can be passed any object that
implements this class. In IFTool the context will draw to a bitmap in
memory, but another program using the same interface might for example
draw directly to the screen. When defining a plugin interface, it\'s
well worth sticking to the simple rule of not limiting the client,
which can be achieved by defining operations as generically as possible
(within the bounds of reason, of course). If you ignore this rule, it
will quickly come back to haunt you in practice.
Listing 2. Method definitions for the CIFToolPlugIn class
#include "IFToolPlugIn.h"
#include <Fbs.h>
EXPORT_C CIFToolPlugIn::CIFToolPlugIn()
{
}
EXPORT_C CIFToolPlugIn::~CIFToolPlugIn()
{
// Releases the resources allocated for this object
// and notifies ECOM that the plugin instance has been
// destroyed
REComSession::DestroyedImplementation( iDtor_ID_Key );
}
EXPORT_C CIFToolPlugIn* CIFToolPlugIn::NewL()
{
// Default plugin - an averaging filter
const TUid KCIFToolPlugIn_AveragingFilterUid
= { 0x20000ACA };
TAny* defaultIFToolPlugin =
REComSession::CreateImplementationL(
KCIFToolPlugIn_AveragingFilterUid,
_FOFF( CIFToolPlugIn, iDtor_ID_Key ) );
return reinterpret_cast< CIFToolPlugIn*>(
defaultIFToolPlugin );
}
EXPORT_C CIFToolPlugIn* CIFToolPlugIn::NewL( const TDesC8& aCue )
{
// Use the default ECOM resolver
TEComResolverParams resolverParams;
resolverParams.SetDataType( aCue );
// Allow wildcards in the hint string
resolverParams.SetWildcardMatch( ETrue );
TAny* ifToolPlugIn =
REComSession::CreateImplementationL(
KCIFToolPlugIn_InterfaceUid,
_FOFF( CIFToolPlugIn, iDtor_ID_Key ),
NULL, resolverParams );
return reinterpret_cast< CIFToolPlugIn*>(
ifToolPlugIn );
}
EXPORT_C void CIFToolPlugIn::ListImplementationsL(
RImplInfoPtrArray& aImplInfoArray )
{
// Fetch a list of available implementations
REComSession::ListImplementationsL(
KCIFToolPlugIn_InterfaceUid,
aImplInfoArray );
}
EXPORT_C void CIFToolPlugIn::DoFilterL(
const CFbsBitmap& aSourceBitmap,
CGraphicsContext& aOutputGc )
{
TInt sourceBitmapWidth
= aSourceBitmap.SizeInPixels().iWidth;
TInt sourceBitmapHeight
= aSourceBitmap.SizeInPixels().iHeight;
TPoint pixel;
aOutputGc.SetPenSize( TSize( 1, 1 ) );
for ( pixel.iX = 1; pixel.iX < sourceBitmapWidth;
++pixel.iX )
{
for ( pixel.iY = 1; pixel.iY < sourceBitmapHeight;
++pixel.iY )
{
aOutputGc.SetPenColor(
FilterPixelL( aSourceBitmap, pixel ) );
aOutputGc.Plot( pixel );
}
}
}
// DLL entry point
GLDEF_C TInt E32Dll( TDllReason /*aReason*/ )
{
return KErrNone;
}
In the light of the method description presented above,
you may be wondering what the IMPORT_C modifier is for and why
DoFilterL() is not declared as virtual. Allow me to explain. IMPORT_C
means that the method will be exported out of a DLL, i.e. it will be
available for clients to call as part of the DLL\'s interface.
Explaining the non-virtual nature of the method requires a brief
digression. The idea is that the plugin method expected by the client
need not necessarily be the actual same method that provides unique
plugin functionality. For our sample application, we are assuming that
filtering will take place at pixel level, so defining DoFilterL() as a
pure virtual method would require each plugin implementation to supply
its own code for reading subsequent pixels, thus quite clearly
duplicating functionality between implementations. Note that for our
filter plugins we\'re also assuming that operations on subsequent pixels
do not modify the source bitmap, so the order in which pixels are
processed is irrelevant to the resulting image. This makes it possible
to define the private and pure virtual FilterPixelL() method which is
uniquely implemented for each plugin and provides the actual filtering
operation for particular pixels. If you take a look at the
implementation of DoFilterL() in Listing 2, you can see FilterPixelL()
being called for each source pixel in turn. This approach to designing
base classes is quite common - it is in fact the Template method design
pattern.
Getting back to the main story, a class implementing the ECOM interface has to:
-
be abstract, i.e. contain at least one pure virtual method;
-
provide at least one factory class, allowing clients to fetch a specified implementation;
-
provide
a means of releasing allocated resources, for example through a
destructor or a dedicated clean-up method (e.g. Close());
-
contain a member of type TUid, required for implementing automatic resource release.
Let\'s go over these requirements, comparing them to the
source code in Listing 2. The abstractness requirement is pretty
obvious - without it there would be little point in creating a plugin
in the first place. The factory method implementations are in line with
the two-phase construction model in Symbian OS. Our sample code uses
two versions of the NewL() method. The first version takes no arguments
and handles the creation of a default plugin - an unofficial
requirement when working with ECOM. The key operation within the method
is the call to REComSession::CreateImplementationL(), which is passed
the plugin identifier (UID) and the address of a TUid member fetched
using the _FOFF macro. The call returns the ECOM pointer to the default
plugin implementation (in this case an averaging filter). The returned
pointer type is TAny* (the Symbian equivalent of void*), so before the
plugin is passed to the client it needs to be cast to the same type as
the plugin interface (in our case CIFToolPlugIn). For this application,
I\'ve used the reinterpret_cast operator to do the actual cast.
Listing 3. IFToolPlugIn component definition
TARGET IFToolPlugIn.dll
TARGETTYPE dll
UID 0x1000008D 0x20000AC7
USERINCLUDE ..inc
SYSTEMINCLUDE epoc32include
SYSTEMINCLUDE epoc32includeecom
SOURCEPATH ..src
SOURCE IFToolPlugIn.cpp
LIBRARY ECom.lib
LIBRARY EUser.lib
LIBRARY FbsCli.lib
LIBRARY Gdi.lib
The overloaded version of the factory method takes one
argument: a descriptor (the Symbian OS equivalent of a string)
providing a hint concerning plugin selection. Looking at the method
body, you can see the construction of a TEComResolverParams object.
Objects of this class identify particular plugins, making it possible
for ECOM to select the right implementation. The remainder of the
method body is similar to that of the default version, except that
REComSession::CreateImplementationL() is called with two extra
arguments. The first parameter can be used whenever the construction of
a specific plugin requires external data to be passed - since we have
no need of that, we can pass NULL. The other parameter is the
TEComResolverParams object discussed above, providing a hint concerning
plugin selection.
The next method to examine is ListImplementationsL(),
which calls REComSession::ListImplementationsL() to fetch ECOM\'s list
of available plugin implementations for the interface with the
specified UID and writes that list to the address passed in the
argument. You will notice that implementing ECOM plugins involves using
a variety of UIDs for a variety of purposes, which can at first be a
bit confusing. See the Frame ECOM and UIDs for a description of all the
identifiers used in the examples - reading it once you\'re familiar with
the article will help you get a better overall picture of the whole
scheme.
Listing 4. IFToolPlugInsDefault component definition
TARGET IFToolPlugInsDefault.dll
TARGETTYPE ECOMIIC
UID 0x10009D8D 0x20000AC9
SOURCEPATH ..src
SOURCE IFToolPlugIn_AveragingFilter.cpp
SOURCE IFToolPlugIn_MedianFilter.cpp
SOURCE IFToolPlugInsDefault_Main.cpp
SOURCE IFToolPlugInsDefault_Proxy.cpp
SOURCEPATH ..data
RESOURCE 20000AC9.rss
USERINCLUDE ..inc
SYSTEMINCLUDE epoc32include
SYSTEMINCLUDE epoc32includeecom
LIBRARY EUser.lib
LIBRARY FbsCli.lib
LIBRARY Gdi.lib
LIBRARY IFToolPlugIn.lib
To fulfil the requirements of the ECOM interface, we also
need a destructor. Looking at Listing 2, you can see that destroying a
plugin involves calling REComSession::DestroyedImplementation() with the magic argument iDtor_ID_Key.
To complete this discussion of the ECOM plugin interface,
have a look at the definition file for the IFPlugIn component, shown in
Listing 3. The ECOM interface is physically implemented as a DLL with a
static interface, as indicated by the value 0x1000008D in the UID line.
The component is also linked to the ECOM library in the line:
LIBRARY ECom.lib
Implementing the ECOM interface
With the ECOM interface ready, we can set about
implementing some actual plugins. ECOM allows multiple plugins to be
defined within one DLL, so all the plugins for our sample application
will be implemented within the IFPlugInsDefault component. Listing 4
presents the contents of the component definition file. Here are the
four most important entries:
-
TARGETTYPE ECOMIIC: the component is a DLL containing an ECOM interface implementation;
-
UID 0x10009D8D 0x10009EE1: the first value is a constant library type, while the second is the current component\'s unique UID;
-
RESOURCE 20000AC9.rss: a link to the current implementation\'s resource file (discussed below);
-
LIBRARY ECom.lib: information for the linker that the component requires the ECOM library to be linked.
Now we know the component definition, let\'s have a look
at the source code implementing our two basic plugins (Listings 5 and
6). The component is implemented as a DLL, so we also need to define a
suitable entry point:
TBool E32Dll()
{
return (ETrue);
}
The definition resides in the source file IFToolPlugInsDefault_Main.cpp.
As already mentioned, one ECOM interface implementation
can contain several actual plugin definitions. For the purpose of our
example, the IFPlugIns component implements two plugins: an averaging
filter and a median filter. Although both filters are very simple,
discussing the details of their operation is beyond the scope of this
article, so for more information on them you can consult external
resources or simply study the IFTool source. Listing 5 contains the
class definition for the averaging filter plugin - the class extends
CIFToolPlugIn, but apart from that it\'s pretty straightforward.
Listing 5. ECOM plugin implementation - CIFToolPlugin_AveragingFilter class definition
#ifndef __IFTOOL_PLUG_IN__AVERAGING_FILTER_H__
#define __IFTOOL_PLUG_IN__AVERAGING_FILTER_H__
#include <IFTool/IFToolPlugIn.h>
class CIFToolPlugin_AveragingFilter
: public CIFToolPlugIn
{
public:
static CIFToolPlugin_AveragingFilter* NewL();
~CIFToolPlugin_AveragingFilter();
private: // Plugin interface implementation
TRgb FilterPixelL( const CFbsBitmap& aSourceBitmap,
TPoint aPixelPoint );
private:
CIFToolPlugin_AveragingFilter();
void ConstructL();
void InitializeMask();
private:
TInt iMask